import { BitFieldUtil } from '@boticord/common'
import {
  Bot,
  PendingBot,
  Report,
  Resource,
  ResourceComment,
  ResourceStatus,
  ResourceUp,
  Server,
} from '@boticord/resources-client'
import { Badge, User } from '@boticord/users-client'
import {
  AbilityBuilder,
  ExtractSubjectType,
  MatchConditions,
  PureAbility,
  Subject,
} from '@casl/ability'
import { Injectable } from '@nestjs/common'
import { Token } from '../models/token.model'
import { Permission } from './permission.enum'
import { BoticordAbility } from './ability.type'
import { Action } from './action.enum'

@Injectable()
export class CaslAbilityFactory {
  private readonly abilityCache = new Map<string, BoticordAbility>()
  
  private notPrivate(status: ResourceStatus) {
    return status !== ResourceStatus.Banned && status !== ResourceStatus.Pending
  }

  private notBanned(status: ResourceStatus) {
    return status !== ResourceStatus.Banned
  }
  
  private idMatch(from: any[], to: string) {
    return from.some((item) => item.id === to)
  }
  
  forAnonymous() {
    const cache = this.abilityCache.get('0')
    if (cache) return cache

    const builder = new AbilityBuilder<BoticordAbility>(PureAbility)

    // grant permission to read any resource if it is public (not banned or pending)
    builder.can(Action.Read, [ Server, Bot ], (resource) => this.notPrivate(resource.status))

    const ability = builder.build({
      detectSubjectType:
        (item) => item.constructor as ExtractSubjectType<Subject>,
      conditionsMatcher: (matchConditions: MatchConditions) => matchConditions,
    })

    this.abilityCache.set('0', ability)

    return ability
  }

  forToken(token: Token) {
    const cache = this.abilityCache.get(`${token.id}${token.permissions}`)
    if (cache) return cache

    const builder = new AbilityBuilder<BoticordAbility>(PureAbility)

    const has = (perm: number) => BitFieldUtil.has(token.permissions, perm)

    if (token.isUser()) {
      builder.can(Action.BearerToken, 'all')
      builder.can(Action.Read, User)

      if (has(Permission.ReadDiscordApplication)) {
        // user can read discord application data (approx. server count etc.)
        builder.can(Action.ReadApplication, Bot)
        builder.can(Action.ReadApplication, User)
      }
    } else {
      builder.can(Action.BotToken, 'all')
      // sometimes we're receiving Resource class instead of Bot class, so we need to define permissions for both
      // but when we do it, bot receives permission to modify servers with its id. but there is no such servers,
      // so this is ok
      builder.can(Action.Modify, Resource, (resource) => resource.owner === token.id)
      builder.can(Action.Modify, Bot, (bot) => bot.owner === token.id)

      if (has(Permission.CacheUpdate)) {
        // grant bot token to update cache
        builder.can(Action.UpdateCache, [ Server, Resource, Bot ])
      }
    }

    // grant permission to read any resource if it is public (not banned or pending)
    builder.can(Action.Read, [ Server, Bot ], (resource) => this.notPrivate(resource.status))
    // grant permission to read own resources even if they are banned or pending
    builder.can(
      Action.Read, Server, (server) => server.owner === token.id || this.idMatch(server.moderators, token.id),
    )
    builder.can(
      Action.Read, [ Bot, PendingBot ], (bot) => bot.owner === token.id || this.idMatch(bot.developers, token.id),
    )
    // grant permission to read own permissions
    builder.can(Action.Read, Token, (t) => t.id === token.id)

    if (has(Permission.ModerateReports)) {
      // grant permission to read, read many and change reports statuses
      builder.can([ Action.Read, Action.ReadMany, Action.ChangeStatus ], Report)
    }

    if (has(Permission.Reindex)) {
      builder.can(Action.Reindex, 'all')
    }

    if (has(Permission.PostReports)) {
      // grant permission to create reports
      builder.can(Action.Create, Report)
      // grant permission to edit or delete own reports
      builder.can(
        [ Action.Modify, Action.Delete ],
        Report,
        (report) => report.from.id === token.id,
      )
    }

    if (has(Permission.EditOwnServers)) {
      // grant permission to create servers
      builder.can(Action.Create, Server)
      // grant permission to edit or delete own servers
      builder.can(
        [ Action.Modify, Action.Delete ],
        Server,
        (server) => (this.idMatch(server.moderators, token.id) || server.owner === token.id)
          && this.notPrivate(server.status),
      )
      // grant permission to change own server status, but only if server has not 'banned' or 'pending' status
      builder.can(
        Action.ChangeStatus,
        Server,
        (server) => (this.idMatch(server.moderators, token.id) || server.owner === token.id)
          && this.notPrivate(server.status),
      )
    }

    if (has(Permission.PostComments) || has(Permission.RateResources) || has(Permission.ModerateComments)) {
      // grant permission to create comments
      builder.can(Action.Create, ResourceComment)
      // grant permission to edit or delete own comments
      builder.can(
        [ Action.Modify, Action.Delete ],
        ResourceComment,
        ({ author }) => author.id === token.id, // can modify only own comments
      )
    }

    if (has(Permission.ModerateComments)) {
      // grant permission to edit or delete any comments
      builder.can([ Action.Modify, Action.Delete, Action.ReadMany, Action.Reply ], ResourceComment)
    }

    if (!has(Permission.PostComments) && !has(Permission.ModerateComments)) {
      // remove permission to create or modify comments if resource has content
      builder.cannot(
        [ Action.Create, Action.Modify ],
        ResourceComment,
        ({ content }) => !!content, // cannot set empty content
      )
    }

    if (!has(Permission.RateResources) && !has(Permission.ModerateComments)) {
      // remove permission to create or modify comments if resource has rating
      builder.cannot(
        [ Action.Create, Action.Modify ],
        ResourceComment,
        ({ rating }) => !!rating, // cannot set 0 rating
      )
    }

    if (has(Permission.ModerateServers)) {
      // grant permission to read, modify, delete or change status of any servers
      builder.can([ Action.Read, Action.Create, Action.Modify, Action.Delete, Action.ChangeStatus ], Server)
    }

    if (has(Permission.EditOwnBots)) {
      // grant permission to create bots
      builder.can(Action.Create, [ Bot, PendingBot ])
      // grant permission to edit own bots
      builder.can(
        [ Action.Modify, Action.Delete ],
        Bot,
        (bot) => (this.idMatch(bot.developers, token.id) || bot.owner === token.id) && this.notPrivate(bot.status),
      )
      // grant permission to change own bot status
      builder.can(
        [ Action.ChangeStatus ],
        Bot,
        (bot) => (this.idMatch(bot.developers, token.id) || bot.owner === token.id) && this.notPrivate(bot.status),
      )

      // grant permission to edit own pending bots
      builder.can(
        [ Action.Modify, Action.Delete ],
        PendingBot,
        (bot) => (this.idMatch(bot.developers, token.id) || bot.owner === token.id) && this.notBanned(bot.status),
      )
    }

    if (has(Permission.EditOwnBots) || has(Permission.EditOwnServers)) {
      // grant permission to create reply, modify reply or delete reply if user is owner or moderator of resource
      builder.can(
        Action.Reply,
        ResourceComment,
        ({ resource }) => {
          return this.idMatch((resource as Server).moderators || (resource as Bot).developers, token.id)
            || resource.owner === token.id
        },
      )
    }

    if (has(Permission.ModerateBots)) {
      // grant permission to read, modify, delete or change status of any bots
      builder.can([
        Action.Read, Action.Modify, Action.Delete, Action.ChangeStatus, Action.ReadMany,
      ], [ Bot, PendingBot ])
    }

    if (has(Permission.ManagePermissions)) {
      // grant permission to modify or delete users tokens (user for permissions change or user reset)
      builder.can([ Action.Read, Action.Modify, Action.Delete ], Token)
    }

    if (has(Permission.ManageBadges)) {
      // grant permission to create, modify or delete badges, and to grant these badges to users
      builder.can([ Action.Create, Action.Delete, Action.Modify, Action.Grant ], Badge)
    }

    if (has(Permission.ModerateProfiles)) {
      // grant permission to edit users profiles
      builder.can(Action.Modify, User)
    }

    if (has(Permission.EditProfile)) {
      // grant permission to edit own profile
      builder.can(Action.Modify, User, (user) => user.id === token.id)
    }

    if (has(Permission.Premium)) {
      builder.can(Action.Premium, 'all')
    }

    if (has(Permission.Up)) {
      // grant permission to up resources
      builder.can(Action.Create, ResourceUp)
      // grant permission to remove own up
      // builder.can(Action.Delete, ResourceUp, (up) => up.user.id === token.id) // user can delete only its own up
      // builder.can(Action.Modify, ResourceUp)
      // builder.can(Action.Grant, ResourceUp)
    }

    if (has(Permission.ModerateUps)) {
      // grant permission to remove any up
      builder.can(Action.Delete, ResourceUp)
    }
    
    const ability = builder.build({
      detectSubjectType: 
        (item) => item.constructor as ExtractSubjectType<Subject>,
      conditionsMatcher: (matchConditions: MatchConditions) => matchConditions,
    })

    this.abilityCache.set(`${token.id}${token.permissions}`, ability)

    return ability
  }
}
